package org.anno.chunkdb;

import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;

import java.io.IOException;
import java.io.OutputStream;
import java.sql.SQLException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Replies to SQL queries.
 */
@Singleton
public class QueryServlet extends HttpServlet {

  enum Format {
    CSV, HTML;

    ResponseVisitor getVisitor(OutputStream out) throws IOException {
      switch (this) {
        case CSV:
          return new ResponseVisitor.Csv(out);
        case HTML:
          int orderByColumn = -1;
          boolean ascending = true;
          return new ResponseVisitor.Html(out, orderByColumn, ascending);
        default:
          throw new IllegalArgumentException();
      }
    }
  }

  private final TableDao dao;
  private final QueryParser parser;

  @Inject
  public QueryServlet(TableDao dao, QueryParser parser) {
    this.dao = dao;
    this.parser = parser;
  }

  private QueryServlet(TableDao dao) {
    this(dao, new QueryParser(dao));
  }

  @VisibleForTesting
  public QueryServlet() {
    this(new TableDao());
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    Format f = getFormat(req);
    String sql = req.getParameter("sql");
    if (sql == null) {
      resp.sendError(HttpServletResponse.SC_NOT_FOUND, "sql parameter required");
      return;
    }
    try {
      Chunkdb.Select select = parser.parse(sql);
      Metadata.Table table = dao.get(select.getFrom());
      Function<byte[], QueryPlan> planMaker = getPlanMaker(select, table);
      for (byte[] chunk : dao.getChunks(select.getFrom())) {
        // TODO merge ordered results
        planMaker.apply(chunk).accept(f.getVisitor(resp.getOutputStream()));
      }
    } catch (EntityNotFoundException | SQLException e) {
      resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
    }
  }

  Function<byte[], QueryPlan> getPlanMaker(Chunkdb.Select select, final Metadata.Table table) {
    // TODO needs much more work
    if (select.hasOrderBy()) {
      Object2IntMap<String> indexByName = getColumnIndexByName(table);
      final int i = indexByName.getInt(select.getOrderBy());
      if (i >= 0) {
        return chunk -> new IndexScanPlan(table, i, chunk, true);
      }
    }
    return input -> new ChunkScanPlan(table, input);
  }

  static Object2IntMap<String> getColumnIndexByName(Metadata.Table table) {
    Object2IntOpenHashMap<String> result = new Object2IntOpenHashMap<>();
    result.defaultReturnValue(-1);
    for (int i = table.getColumnCount(); --i >= 0; ) {
      result.put(table.getColumn(i).getName(), i);
      result.put("col" + i, i);
    }
    return result;
  }

  private static Format getFormat(HttpServletRequest req) {
    try {
      return Format.valueOf(req.getParameter("format"));
    } catch (IllegalArgumentException | NullPointerException e) {
    }
    return Format.CSV;
  }
}
